using System;
using System.Net;
using System.Resources;
using System.Text;
using System.Runtime.InteropServices;
using Microsoft.TeamFoundation.Client;

namespace Microsoft.TeamFoundation.Migration.Toolkit
{
    /// <summary>
    /// Crendential UI for prompting user for user name and password.
    /// </summary>
    public class StoredCredentialsProvider : ICredentialsProvider
    {
        private const int Win2000VersionMajor = 5;
        private const int Win2000VersionMinor = 0;
        private ICredentialsProvider provider; //Bridge for providing the os specific credentials
        private int attempts = 0;

        public StoredCredentialsProvider()
        {
            this.provider = StoredCredentialsProvider.GetProvider();
        }

        /// <summary>
        /// Factory for getting the credentials provider
        /// </summary>
        /// <returns></returns>
        private static ICredentialsProvider GetProvider()
        {
            try
            {
                OperatingSystem os = System.Environment.OSVersion;
                if (os.Platform == PlatformID.Win32NT &&
                    os.Version.Major == Win2000VersionMajor &&
                    os.Version.Minor == Win2000VersionMinor)
                {
                   // return new Win2000CredentialsProvider();
                }
            }
            catch (ExecutionEngineException)
            {
            }

            //Fallback provider for any other os.
            return new Win32CredentialsProvider();
        }

        /// <summary>
        /// Get the name (and domain) of the failed user
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="failedCredentials"></param>
        /// <returns></returns>
        internal static String FailedUserName(Uri uri, ICredentials failedCredentials)
        {
            String userName = String.Empty;

            if (failedCredentials != null)
            {
                NetworkCredential cred = failedCredentials.GetCredential(uri, null);
                if (cred != null)
                {
                    if (cred.Domain.Length > 0 && cred.UserName.Length > 0)
                    {
                        userName = cred.Domain + "\\" + cred.UserName;
                    }
                    else
                    {
                        userName = cred.UserName;
                    }
                }
            }

            return userName;
        }

        /// <summary>
        /// The team foundation objects call this method to get the credentials if the TFS object's
        /// current credentials failed to authenticate with the server.
        /// Note that the HttpWebRequest.Credentials property requires that the object be either a
        /// NetworkCredential or CredentialCache.
        /// </summary>
        /// <param name="uri">The Uri of the server.</param>
        /// <param name="failedCredentials">The crendentials that failed to authenticate (may be
        ///   null, default credentials which return empty string for user name, a CredentialCache
        ///   object, or a NetworkCredential object).  Functions like CredUIPromptForCredentials()
        ///   can use the failed credentials to populate the user name box in the dialog.</param>
        /// <returns>The credential to use when authenticating with the server.</returns>
        ICredentials ICredentialsProvider.GetCredentials(Uri uri, ICredentials failedCredentials)
        {

            if (attempts > 0)
            {
                return null;
            }
            else
            {
                attempts++;
            }


            if (this.provider != null)
            {
                return this.provider.GetCredentials(uri, failedCredentials);
            }

            return null;
        }

        /// <summary>
        /// Called by the team foundation objects if the credentials supplied by GetCredential()
        /// were successfully authenticated by the server.  If the credentials object supplied by
        /// GetCredentials was a CredentialCache object, you don't know which authenticated.
        /// </summary>
        void ICredentialsProvider.NotifyCredentialsAuthenticated(Uri uri)
        {
            if (this.provider != null)
            {
                this.provider.NotifyCredentialsAuthenticated(uri);
            }
        }
    }

    /// <summary>
    ///  Wrap Win32 Crendential UI for prompting user for user name and password.
    /// </summary>
    internal class Win32CredentialsProvider : ICredentialsProvider
    {
        internal Win32CredentialsProvider()
        {
        }

        /// <summary>
        /// The team foundation objects call this method to get the credentials if the TFS object's
        /// current credentials failed to authenticate with the server.
        /// Note that the HttpWebRequest.Credentials property requires that the object be either a
        /// NetworkCredential or CredentialCache.
        /// </summary>
        /// <param name="uri">The Uri of the server.</param>
        /// <param name="failedCredentials">The crendentials that failed to authenticate (may be
        ///   null, default credentials which return empty string for user name, a CredentialCache
        ///   object, or a NetworkCredential object).  Functions like CredUIPromptForCredentials()
        ///   can use the failed credentials to populate the user name box in the dialog.</param>
        /// <returns>The credential to use when authenticating with the server.</returns>
        ICredentials ICredentialsProvider.GetCredentials(Uri uri, ICredentials failedCredentials)
        {
            if (uri == null)
            {
                throw new ArgumentNullException("uri");
            }

            ICredentials credentials = null;
            CREDUI_INFO credirUR = new CREDUI_INFO();
            StringBuilder userName = new StringBuilder("", CredUIHelper.CREDUI_MAX_USERNAME_LENGTH);
            StringBuilder password = new StringBuilder("", CredUIHelper.CREDUI_MAX_PASSWORD_LENGTH);
            int iSave = 0;
            
            credirUR.cbSize = Marshal.SizeOf(credirUR);

            credirUR.pszCaptionText = String.Format(MigrationToolkitResources.UICredProvider_TitleText,
                                                              uri.Host);
            if (credirUR.pszCaptionText.Length > CredUIHelper.CREDUI_MAX_CAPTION_LENGTH)
            {
                credirUR.pszCaptionText = credirUR.pszCaptionText.Substring(0,
                                                           CredUIHelper.CREDUI_MAX_CAPTION_LENGTH);
            }

            
            credirUR.pszMessageText = MigrationToolkitResources.UICredProvider_MessageText;
            if (credirUR.pszMessageText.Length > CredUIHelper.CREDUI_MAX_MESSAGE_LENGTH)
            {
                credirUR.pszMessageText = credirUR.pszMessageText.Substring(0,
                                                           CredUIHelper.CREDUI_MAX_MESSAGE_LENGTH);
            }
            credirUR.hbmBanner = IntPtr.Zero;

            userName.Append(StoredCredentialsProvider.FailedUserName(uri, failedCredentials));

            CredUIReturnCodes rtn =
                CredUIHelper.CredUIPromptForCredentials
                    (ref credirUR,
                    uri.AbsoluteUri,
                    IntPtr.Zero,
                    0,
                    userName,
                    CredUIHelper.CREDUI_MAX_USERNAME_LENGTH,
                    password,
                    CredUIHelper.CREDUI_MAX_PASSWORD_LENGTH,
                    ref iSave,
                    CREDUI_FLAGS.GENERIC_CREDENTIALS
                    | CREDUI_FLAGS.EXCLUDE_CERTIFICATES
                    | CREDUI_FLAGS.PERSIST                  
                    );

            if (rtn != CredUIReturnCodes.NO_ERROR)
            {
                credentials = null;
            }
            else
            {
                credentials = ConvertToCredential(userName.ToString(), password.ToString());

                // clear the password
                password.Remove(0, password.Length);
            }

            return credentials;
        }

        /// <summary>
        /// Called by the team foundation objects if the credentials supplied by GetCredential()
        /// were successfully authenticated by the server.  If the credentials object supplied by
        /// GetCredentials was a CredentialCache object, you don't know which authenticated.
        /// </summary>
        void ICredentialsProvider.NotifyCredentialsAuthenticated(Uri uri)
        {
        }

        // Internal helper function to parse domain and user from username.  It returns a NetworkCredential with those settings.
        internal static NetworkCredential ConvertToCredential(String username, String password)
        {
            StringBuilder user = new StringBuilder("", CredUIHelper.CREDUI_MAX_USERNAME_LENGTH);
            StringBuilder domain = new StringBuilder("", CredUIHelper.CREDUI_MAX_DOMAIN_LENGTH);

            //parse domain / user name
            CredUIReturnCodes rtn;
            rtn = CredUIHelper.CredUIParseUserName(username,
                                                   user,
                                                   CredUIHelper.CREDUI_MAX_USERNAME_LENGTH,
                                                   domain,
                                                   CredUIHelper.CREDUI_MAX_DOMAIN_LENGTH);

            NetworkCredential cred;
            if (rtn == CredUIReturnCodes.NO_ERROR)
            {
                cred = new NetworkCredential(user.ToString(), password, domain.ToString());
            }
            else
            {
                cred = new NetworkCredential(username, password);
            }

            return cred;
        }
    }

    [Flags]
    internal enum CREDUI_FLAGS
    {
        INCORRECT_PASSWORD = 0x1,
        DO_NOT_PERSIST = 0x2,
        REQUEST_ADMINISTRATOR = 0x4,
        EXCLUDE_CERTIFICATES = 0x8,
        REQUIRE_CERTIFICATE = 0x10,
        SHOW_SAVE_CHECK_BOX = 0x40,
        ALWAYS_SHOW_UI = 0x80,
        REQUIRE_SMARTCARD = 0x100,
        PASSWORD_ONLY_OK = 0x200,
        VALIDATE_USERNAME = 0x400,
        COMPLETE_USERNAME = 0x800,
        PERSIST = 0x1000,
        SERVER_CREDENTIAL = 0x4000,
        EXPECT_CONFIRMATION = 0x20000,
        GENERIC_CREDENTIALS = 0x40000,
        USERNAME_TARGET_CREDENTIALS = 0x80000,
        KEEP_USERNAME = 0x100000
    }

    internal enum CredUIReturnCodes
    {
        NO_ERROR = 0,
        ERROR_CANCELLED = 1223,
        ERROR_NO_SUCH_LOGON_SESSION = 1312,
        ERROR_NOT_FOUND = 1168,
        ERROR_INVALID_ACCOUNT_NAME = 1315,
        ERROR_INSUFFICIENT_BUFFER = 122,
        ERROR_INVALID_PARAMETER = 87,
        ERROR_INVALID_FLAGS = 1004
    }

    internal enum CRED_TYPE
    {
        GENERIC = 1,
        CRED_TYPE_DOMAIN_PASSWORD = 2,
        CRED_TYPE_DOMAIN_CERTIFICATE = 3,
        CRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 4
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct CREDUI_INFO
    {
        public int cbSize;
        public IntPtr hwndParent;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string pszMessageText;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string pszCaptionText;
        public IntPtr hbmBanner;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct CREDENTIAL
    {
        public int Flags;
        public int Type;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string TargetName;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string Comment;
        public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
        public int CredentialBlobSize;
        public IntPtr CredentialBlob;
        public int Persist;
        public int AttributeCount;
        public IntPtr Attributes;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string TargetAlias;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string UserName;
    }



    // helper class to wrap Win32 APIs
    internal class CredUIHelper
    {
        // Maximum length of the UserName field.  The worst case is <User>@<DnsDomain>
        public const int CREDUI_MAX_USERNAME_LENGTH = 256 + 1 + 256;
        public const int CREDUI_MAX_PASSWORD_LENGTH = 256;
        public const int CREDUI_MAX_DOMAIN_LENGTH = 256;

        public const int CREDUI_MAX_MESSAGE_LENGTH = 32767;
        public const int CREDUI_MAX_CAPTION_LENGTH = 128;
        private const int UNICODE_BYTE = 2;

        [DllImport("credui.dll",
                   EntryPoint = "CredUIPromptForCredentialsW",
                   CharSet = CharSet.Unicode
                   )]
        public static extern CredUIReturnCodes CredUIPromptForCredentials
                (ref CREDUI_INFO credirUR,
                      string targetName,
                      IntPtr reserved1,
                      int iError,
                      StringBuilder userName,
                      int maxUserName,
                      StringBuilder password,
                      int maxPassword,
                  ref int iSave,
                      CREDUI_FLAGS flags);

        [DllImport("credui.dll",
                  EntryPoint = "CredUICmdLinePromptForCredentialsW",
                  CharSet = CharSet.Unicode
                   )]
        public static extern CredUIReturnCodes CredUICmdLinePromptForCredentials
                  (string targetName,
                      IntPtr reserved1,
                      int iError,
                      StringBuilder userName,
                      int maxUserName,
                      StringBuilder password,
                      int maxPassword,
                  ref int iSave,
                      CREDUI_FLAGS flags);

        [DllImport("credui.dll",
                  EntryPoint = "CredUIConfirmCredentialsW",
                  CharSet = CharSet.Unicode
                   )]
        public static extern CredUIReturnCodes CredUIConfirmCredentials
               (string targetName,
                [MarshalAs(UnmanagedType.Bool)]
                bool confirm);

        [DllImport("credui.dll",
                  EntryPoint = "CredUIParseUserNameW",
                  CharSet = CharSet.Unicode
                   )]
        public static extern CredUIReturnCodes CredUIParseUserName
                (string userName,
                   StringBuilder user,
                   int maxUser,
                   StringBuilder domain,
                   int maxDomain);
    }
}
